package away3d.containers ;

import away3d.cameras.Camera3D ;
import away3d.cameras.SpringCam ;
import away3d.core.managers.Mouse3DManager ;
import away3d.core.managers.Stage3DManager ;
import away3d.core.managers.Stage3DProxy ;
import away3d.core.render.DefaultRenderer ;
import away3d.core.render.DepthRenderer ;
import away3d.core.render.HitTestRenderer ;
import away3d.core.render.RendererBase ;
import away3d.core.traverse.EntityCollector ;
import away3d.filters.Filter3DBase ;
import away3d.lights.LightBase ;
import flash.display.Sprite ;
import flash.display3D.Context3D ;
import flash.display3D.Context3DTextureFormat ;
import flash.display3D.textures.Texture ;
import flash.events.Event ;
import flash.geom.Vector3D ;
import flash.utils.getTimer ;



class View3D extends Sprite
{
    
    
    private var _width                  : Float ;
    private var _height                 : Float ;
    private var _scaleX                 : Float ;
    private var _scaleY                 : Float ;
    private var _x                      : Float ;
    private var _y                      : Float ;
    private var _scene                  : Scene3D ;
    private var _camera                 : Camera3D ;
    private var _entityCollector        : EntityCollector ;
    private var _aspectRatio            : Float ;
    private var _time                   : Float ;
    private var _deltaTime              : Int ;
    private var _backgroundColor        : Int ;
    private var _hitManager             : Mouse3DManager ;
    private var _stage3DManager         : Stage3DManager ;
    private var _renderer               : RendererBase ;
    private var _depthRenderer          : DepthRenderer ;
    private var _hitTestRenderer        : HitTestRenderer ;
    public var _addedToStage            : Bool ;
    private var _filters3d              : Array ;
    private var _requireDepthRender     : Bool ;
    private var _depthRender            : Texture ;
    private var _depthTextureWidth      : Int ;
    private var _depthTextureHeight     : Int ;
    private var _depthTextureInvalid    : Bool ;
    private var _hitField               : Sprite ;
    public var mouseZeroMove            : Bool ;
    
    
    public function new( ?scene : Scene3D = null, ?camera : Camera3D = null, ?renderer : DefaultRenderer = null )
    {
        
        super() ;
        
        _width                  = 0 ;
        _height                 = 0 ;
        _scaleX                 = 1 ;
        _scaleY                 = 1 ;
        _x                      = 0 ;
        _y                      = 0 ;
        _time                   = 0 ;
        _backgroundColor        = 0x000000 ;
        _depthTextureWidth      = -1 ;
        _depthTextureHeight     = -1 ;
        _depthTextureInvalid    = true ;
        _scene                  = scene || new Scene3D() ;
        _camera                 = camera || new Camera3D() ;
        _renderer               = renderer || new DefaultRenderer( 0 ) ;
        _hitTestRenderer        = new HitTestRenderer() ;
        _depthRenderer          = new DepthRenderer() ;
        _entityCollector        = new EntityCollector() ;
        
        initHitField() ;
        
        addEventListener( Event.ADDED_TO_STAGE,     onAddedToStage,     false, 0, true ) ;
        addEventListener( Event.REMOVED_FROM_STAGE, onRemovedFromStage, false, 0, true ) ;
        
    }
    
    
    private function initHitField()
    {
        
        _hitField       = new Sprite() ;
        _hitField.alpha = 0 ;
        
        _hitField.doubleClickEnabled = true ;
        _hitField.graphics.beginFill( 0x000000 ) ;
        _hitField.graphics.drawRect( 0, 0, 100, 100 ) ;
        
        addChild( _hitField ) ;
        
    }
    
    
    /**
     * Not supported. Use filters3d instead.
     */
    override private function get_filters() : Array
    {
        
        throw new Error( "filters is not supported in View3D. Use filters3d instead." ) ;
        return super.filters ;
        
    }
    
    
    /**
     * Not supported. Use filters3d instead.
     */
    override private function set_filters( value : Array )
    {
        
        throw new Error( "filters is not supported in View3D. Use filters3d instead." ) ;
        
    }
    
    
    public var filters3d( get_filters3d, set_filters3d ): Array<
    
    
    private function get_filters3d() : Array
    {
        
        return _filters3d ;
        
    }
    
    
    private function set_filters3d( value : Array )
    {
        
        var len             = value.length ;
        _filters3d          = value ;
        _requireDepthRender = false ;
        
        for( i in 0...len )
        {
            
            //TODO: change this line!!
            _requireDepthRender ||= _filters3d[i].requireDepthRender ;
            
        }
        
    }
    
    
    public var renderer( get_renderer, set_renderer ) : RendererBase ;
    
    
    /**
     * The renderer used to draw the scene.
     */
    private function get_renderer() : RendererBase
    {
        return _renderer ;
    }

    private function set_renderer( value : RendererBase )
    {
        
        var stage3DProxy : Stage3DProxy = _renderer.stage3DProxy ;
        _renderer.dispose() ;
        _renderer = value ;
        _renderer.stage3DProxy = stage3DProxy ;
        _depthRenderer.stage3DProxy = stage3DProxy ;
        _renderer.viewPortX = _x ;
        _renderer.viewPortY = _y ;
        _depthRenderer.backBufferWidth = _renderer.backBufferWidth = _width ;
        _depthRenderer.backBufferHeight = _renderer.backBufferHeight = _height ;
        _depthRenderer.viewPortWidth = _renderer.viewPortWidth = _width*_scaleX ;
        _depthRenderer.viewPortHeight = _renderer.viewPortHeight = _height*_scaleY ;
        _renderer.backgroundR = ( ( _backgroundColor >> 16 ) & 0xff ) / 0xff ;
        _renderer.backgroundG = ( ( _backgroundColor >> 8 ) & 0xff ) / 0xff ;
        _renderer.backgroundB = ( _backgroundColor & 0xff ) / 0xff ;
        return value ;
        
    }
    
    
    public var backgroundColor( get_backgroundColor, set_backgroundColor ) : Int
    
    
    /**
     * The background color of the screen. This value is only used when clearAll is set to true.
     */
    private function get_backgroundColor() : Int
    {
        
        return _backgroundColor ;
        
    }
    
    
    private function set_backgroundColor( value : Int )
    {
        
        _backgroundColor        = value ;
        _renderer.backgroundR   = ( ( value >> 16 ) & 0xff ) / 0xff ;
        _renderer.backgroundG   = ( ( value >> 8 ) & 0xff ) / 0xff ;
        _renderer.backgroundB   = ( value & 0xff ) / 0xff ;
        
    }
    
    
    public var camera( get_camera, set_camera ) : Camera3D ;
    
    
    /**
     * The camera that's used to render the scene for this viewport
     */
    private function get_camera() : Camera3D
    {
        
        return _camera ;
        
    }
    
    
    /**
     * Set camera that's used to render the scene for this viewport
     */
    private function set_camera( camera:Camera3D )
    {
        
        _camera = camera ;
        
    }
    
    
    public var scene( get_scene, null ) : Scene3D ;
    
    
    /**
     * The scene that's used to render for this viewport
     */
    private function get_scene() : Scene3D
    {
        
        return _scene ;
        
    }
    
    
    public var deltaTime( get_deltaTime, null ): Int ;
    
    
    // todo: probably temporary:
    /**
     * The amount of milliseconds the last render call took
     */
    private function get_deltaTime() : Int
    {
        
        return _deltaTime ;
        
    }
    
    
    public var width( get_width, set_width ) : Float ;
    
    
    /**
     * The width of the viewport
     */
    override private function get_width() : Float
    {
        
        return _width ;
        
    }
    
    
    override private function set_width( value : Float ) : Float
    {
        
        _hitTestRenderer.viewPortWidth = value ;
        _renderer.viewPortWidth = value*_scaleX ;
        _renderer.backBufferWidth = value ;
        _depthRenderer.viewPortWidth = value ;
        _depthRenderer.backBufferWidth = value ;
        _hitField.width = value ;
        _width = value ;
        _aspectRatio = _width/_height ;
        _depthTextureInvalid = true ;
        return value ;
        
    }
    
    
    public var height( get_height, set_height ) : Float ;
    
    
    /**
     * The height of the viewport
     */
    override private function get_height() : Float
    {
        
        return _height ;
        
    }
    
    
    override private function set_height( value : Float ) : Float
    {
        
        _hitTestRenderer.viewPortHeight = value ;
        _renderer.viewPortHeight = value*_scaleY ;
        _renderer.backBufferHeight = value ;
        _depthRenderer.viewPortHeight = value ;
        _depthRenderer.backBufferHeight = value ;
        _hitField.height = value ;
        _height = value ;
        _aspectRatio = _width/_height ;
        _depthTextureInvalid = true ;
        return height ;
        
    }
    
    
    public var scaleX( get_scaleX, set_scaleX ) : Float ;
    
    
    override private function get_scaleX() : Float
    {
        
        return _scaleX ;
        
    }
    
    
    override private function set_scaleX( value : Float )
    {
        
        _scaleX = value ;
        _renderer.viewPortWidth = _width*_scaleX ;
        return value ;
        
    }
    
    
    public var scaleY( get_scaleY, set_scaleY ) : Float ;
    
    
    override private function get_scaleY() : Float
    {
        
        return _scaleY ;
        
    }
    
    
    override private function set_scaleY( value : Float )
    {
        
        _scaleY = value ;
        _renderer.viewPortHeight = _height*_scaleY ;
        return value ;
        
    }
    
    
    public var x( get_x, set_x ) : Float ;
    
    
    /**
     * The horizontal coordinate of the top-left position of the viewport.
     */
    override private function get_x() : Float
    {
        
        return _x ;
        
    }
    
    
    override private function set_x( value : Float )
    {
        
        _hitTestRenderer.viewPortX = value ;
        _renderer.viewPortX = value ;
        _x = value ;
        return value ;
        
    }
    
    
    public var y( get_y, set_y ) : Float ;
    
    
    /**
     * The vertical coordinate of the top-left position of the viewport.
     */
    override private function get_y() : Float
    {
        
        return _y ;
        
    }
    
    
    override private function set_y( value : Float )
    {
        
        _hitTestRenderer.viewPortY = value ;
        _renderer.viewPortY = value ;
        _y = value ;
        return value ;
        
    }
    
    
    public var antiAlias( get_antiAlias, set_antiAlias ) : Float ;
    
    
    /**
     * The amount of anti-aliasing to be used.
     */
    private function get_antiAlias() : uint
    {
        
        return _renderer.antiAlias ;
        
    }
    
    
    private function set_antiAlias( value : uint )
    {
        
        _renderer.antiAlias = value ;
        return value ;
        
    }
    
    
    /**
     * The amount of faces that were pushed through the render pipeline on the last frame render.
     */
    private function get_renderedFacesCount() : Int
    {
        
        return _entityCollector.numTriangles ;
        
    }
    
    
    /**
     * Renders the view.
     */
    public function render()
    {
        
        var time                    = getTimer() ;
        var targetTexture   : Texture ;
        var numFilters              = _filters3d? _filters3d.length : 0 ;
        var context                 = _renderer.context ;
        
        if( _time == 0 ) _time      = time ;
        
        _deltaTime                  = time - _time ;
        _time                       = time ;
        
        _entityCollector.clear() ;
        
        _camera.lens.aspectRatio    = _aspectRatio ;
        _entityCollector.camera     = _camera ;
        _scene.traversePartitions( _entityCollector ) ;
        
        updateLights( _entityCollector ) ;
        
        if( _requireDepthRender )
        {
            
            renderSceneDepth( _entityCollector ) ;
            
        }
        
        if( numFilters > 0 && context )
        {
            
            var nextFilter  : Filter3DBase ;
            var filter      = Filter3DBase( _filters3d[ 0 ] ) ;
            targetTexture   = filter.getInputTexture( context, this ) ;
            
            _renderer.render( _entityCollector, targetTexture ) ;

            for ( i in 1...numFilters )
            {
                
                nextFilter = i < numFilters? Filter3DBase( _filters3d[ i ] ) : null ;
                
                filter.render( context, nextFilter? nextFilter.getInputTexture( context, this ) : null, _camera, _depthRender ) ;
                filter = nextFilter ;
                
            }
            
            context.present() ;
            
        }
        else
        {
            
            _renderer.render( _entityCollector ) ;
            
        }
        
        _entityCollector.cleanUp() ;
        
        fireMouseMoveEvent() ;
        
    }
    
    
    /**
     * Manually fires a mouseMove3D event.
     */
    public function fireMouseMoveEvent( ?force: Bool = false )
    {
        
        _hitManager.fireMouseMoveEvent( force ) ;
        
    }
    
    
    private function renderSceneDepth( entityCollector : EntityCollector )
    {
        
        if( _depthTextureInvalid ) initDepthTexture( _renderer.context ) ;
        _depthRenderer.render( entityCollector, _depthRender ) ;
        
    }
    
    
    private function initDepthTexture( context : Context3D )
    {
        
        var w = getPowerOf2Exceeding( _width ) ;
        var h = getPowerOf2Exceeding( _height ) ;
        
        _depthTextureInvalid = false ;
        
        if( w == _depthTextureWidth && h == _depthTextureHeight ) return ;
        
        _depthTextureWidth  = w ;
        _depthTextureHeight = h ;
        
        if( _depthRender ) _depthRender.dispose() ;
        
        _depthRender = context.createTexture( w, h, Context3DTextureFormat.BGRA, true ) ;
        
    }
    
    
    private function getPowerOf2Exceeding( value : int ) : Float
    {
        
        var p = 1 ;
        
        while( p < value )
        {
            
            p <<= 1 ;
            
        }
        
        return p ;
        
    }
    
    
    private function updateLights( entityCollector : EntityCollector )
    {
        
        var lights          = entityCollector.lights ;
        var len             = lights.length ;
        var light           : LightBase ;
        var context         = _renderer.context ;
        var contextIndex    = _renderer.contextIndex ;

        for ( i in 0...len )
        {
            
            light = lights[i] ;
            
            if( light.castsShadows )
            {
                
                light.shadowMapper.renderDepthMap( context, contextIndex, entityCollector, _depthRenderer ) ;
                
            }
            
        }
        
    }
    
    
    /**
     * Disposes all memory occupied by the view. This will also dispose the renderer.
     */
    public function dispose()
    {
        
        _renderer.dispose() ;
        if( _depthRender ) _depthRender.dispose() ;
        
    }
    
    
    public function unproject( mX : Float, mY : Float ) : Vector3D
    {
        
        return _camera.unproject( ( mX * 2 - _width )/_width, ( mY * 2 - _height )/_height  ) ;
        
    }
    
    
    /**
     * The EntityCollector object that will collect all potentially visible entities in the partition tree.
     *
     * @see away3d.core.traverse.EntityCollector
     * @private
     */
    arcane function get entityCollector() : EntityCollector
    {
        return _entityCollector ;
    }
    
    
    /**
     * When added to the stage, retrieve a Stage3D instance
     */
    private function onAddedToStage( event : Event )
    {
        
        if( _addedToStage ) return ;
        
        _addedToStage                   = true ;
        _stage3DManager                 = Stage3DManager.getInstance( stage ) ;
        
        // TO DO: have a stage3D manager?
        if( _width == 0 )   width       = stage.stageWidth ;
        if( _height == 0 )  height      = stage.stageHeight ;
        
        _hitTestRenderer.stage3DProxy   = _stage3DManager.getStage3DProxy( 0 ) ;
        _renderer.stage3DProxy          = _depthRenderer.stage3DProxy = _stage3DManager.getFreeStage3DProxy() ;
        _hitManager                     = new Mouse3DManager( this, _hitTestRenderer ) ;
        
    }
    
    
    /**
     * When removed from the stage, detach from the Stage3D and dispose renderers.
     */
    private function onRemovedFromStage( event : Event )
    {
        
        if( !_addedToStage ) return ;
        
        _addedToStage = false ;
        
        _renderer.stage3DProxy.dispose() ;
        _hitTestRenderer.dispose() ;
        _hitManager.dispose() ;
        
    }
    
    
}